feat: fingerprint-guarded session reuse as default (session: "auto")#18
Merged
Conversation
Previously the provider created a fresh Cursor agent every turn and
re-sent the whole transcript: robust but cache-hostile, increasingly
costly as a conversation grows, and it paid opencode's system prompt on
top of Cursor's own. Opt-in `session: true` resumed one agent per session
but blindly: it could drift from opencode's view of history (message
edits, reverts, opencode-side compaction) and was polluted by non-chat
side calls (e.g. title generation).
Add `session: "auto"` (new default) which hashes only the parts opencode
replays verbatim — the system prompt and the user-message sequence — and
classifies each turn:
- new -> fresh agent, full transcript, pool it
- side-call -> system prompt differs; fresh ephemeral agent, pool untouched
- continuation -> prior user seq is an exact prefix + exactly one new user
message; resume the pooled agent, send only the new message
- divergence -> edit/revert/compaction/queued msgs (or failed resume);
fresh agent + full transcript, re-pool
Worst case on any misclassification is one self-healing full replay —
never worse than the old default. `session: true` is now an alias for
"auto"; `session: false` keeps the always-fresh behavior.
Also include tool outputs (truncated: 2000 chars/result, 500/args) in the
flattened transcript so fresh/divergence/`session: false` replays stay
faithful instead of dropping prior tool results to `[result of X]`
placeholders.
OPENCODE_CURSOR_DEBUG=1 logs per-turn classification and cache usage.
Adds transcript-fingerprint unit tests, rewrites session-pool tests for
the decision-driven acquireAgent contract, and adds a live
session-reuse smoke script.
6f71d05 to
361f152
Compare
The config hook only snapshots opencode's MCP set once at startup, so mid-session enable/disable never reached the Cursor agent. Re-forward the live set from chat.params using client.mcp.status() (runtime truth) + client.config.get() (launch specs), and force a fresh Agent.create when the forwarded set changes between turns (a resumed agent keeps its original servers). Map remote OAuth client registration (clientId/clientSecret/scope) onto the Cursor SDK's auth block so the agent runs its own OAuth flow. opencode's access token never lands in config.mcp, so servers needing OAuth without a shareable clientId (dynamic registration / needs_auth) are skipped and the user is notified via a one-time toast instead of forwarding a spec that 401s.
The pool was in-memory only: an opencode restart lost the fingerprint records, so the first turn of every resumed conversation classified as "new" and paid a cache-cold full-transcript replay even though the Cursor agent (and its conversation, in Cursor's checkpoint store) was still resumable. Persist the records best-effort to ~/.cache/opencode-cursor/session-pool.json following the model-cache pattern (never throws, optimization-only). Records carry updatedAt and are pruned to a 7-day TTL and a 200-entry most-recently-used cap. The in-memory pool lazily hydrates from disk (memory wins on conflict); concurrent opencode processes are last-write-wins on the whole file, where a lost record costs exactly one self-healing full replay. clearAgentPool() now wipes the disk store too; a new resetSessionPoolMemory() test hook simulates process restarts.
The earlier ordering fix (328aecc) closed parts on text<->reasoning transitions, but blocks-mode tool parts were enqueued while the narration part stayed open. Hosts position a part where it STARTED, so text streamed after a tool call appended to the pre-tool part and rendered ABOVE the tool block. Close the open text/reasoning part before emitting tool parts — but only when parts are actually emitted: edit calls buffer until their result (no parts at call time), so the narration part stays alive across that gap instead of splitting needlessly. Adds three regression tests: text/tool/text ordering, reasoning/tool/ reasoning ordering, and the buffered-edit no-split case.
- MCP section: live per-turn forwarding via chat.params (mcp.status + config.get), OAuth client mapping to the Cursor auth block, the unshareable-OAuth skip + one-time toast, and the session-reuse interaction (changed set -> fresh agent; tools sit atop the cache-prefix hierarchy). - Session reuse table: add the MCP-set-changed row. - Cache implications: MCP changes listed among prefix re-seeders. - CHANGELOG: entries for MCP re-forwarding and the tool-block part ordering fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes Cursor session reuse safe enough to be the default, fixing the cost/fidelity problems of the per-turn-fresh path without the robustness risks of blind
session: true.Before:
session: false) created a fresh Cursor agent every turn and re-sent the whole transcript — robust but cache-hostile, increasingly costly as a conversation grows, and paying opencode's system prompt on top of Cursor's own.session: trueresumed one agent per session but blindly: it could drift from opencode's history (edits, reverts, opencode-side compaction) and was polluted by non-chat side calls (title generation).After —
session: "auto"(new default): hashes only the parts opencode replays verbatim (system prompt + user-message sequence) and classifies each turn.newside-callcontinuationdivergenceWorst case on any misclassification is one self-healing full replay — never worse than the old default.
session: trueis now an alias for"auto";session: falsepreserves the always-fresh behavior.Why
opencode re-sends the entire transcript every turn. Cursor caching is automatic and provider-side (no SDK knob); the only lever is prompt-prefix stability across turns — exactly Cursor's documented dispatcher/resume pattern.
"auto"keeps the prefix stable on clean continuations (cache reads dominate, input stays flat) and safely degrades otherwise.Fidelity fix
The flattened-transcript paths (
new/side-call/divergence/session: false) previously dropped Cursor tool results to bare[result of X]placeholders, so a fresh agent re-read a transcript with prior tool outputs missing. Tool outputs are now included, truncated (2,000 chars/result, 500/args) — faithful without unbounded bloat.Implementation
src/provider/transcript-fingerprint.ts(new): hashing +classifyTurn. Hashes system + user messages only — never assistant output (opencode re-serializes our reply unpredictably, which would spuriously mismatch every turn).session-pool.ts: pool stores{ agentId, systemHash, userHashes };acquireAgentis decision-driven (resumeAgentId/poolKey/record); failed resume degrades to fresh-create.language-model.ts:session?: boolean | "auto"default"auto"; classifies each turn and drives resume-vs-fresh + which message to send.message-map.ts: truncated tool-output inclusion.delegate.ts: updated to the newacquireAgentshape.Side-call detection is handled by the system-prompt hash plus the fact that calls without a
sessionIDalready skip pooling; the provider also honors an explicitproviderOptions.cursor.ephemeralflag if a host sets one.Verification
transcript-fingerprintclassification matrix;message-maptruncation; rewrittensession-pooldecision-driven tests).CURSOR_API_KEY=… node scripts/session-reuse-smoke.mjs— expect classificationsfresh:new, resume, resume, fresh:divergence; on continuation turnsinputstays flat whilecacheReaddominates.Observability
OPENCODE_CURSOR_DEBUG=1logs per-turn classification andcacheRead/cacheWriteusage.Docs
README session section rewritten (classification table + cache implications: prefix cache, ~5 min TTL, what re-seeds it). CHANGELOG entry under Unreleased.